﻿using System.ComponentModel.Composition;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using WeavingCommon;

namespace NotifyPropertyWeaverMsBuildTask
{
    [Export, PartCreationPolicy(CreationPolicy.Shared)]
    public class MethodInjector
    {
        WeavingTask weavingTask;
        InterceptorFinder interceptorFinder;
        DelegateHolderInjector delegateHolderInjector;
        MsCoreReferenceFinder msCoreReferenceFinder;

        [ImportingConstructor]
        public MethodInjector(WeavingTask weavingTask, InterceptorFinder interceptorFinder, DelegateHolderInjector delegateHolderInjector, MsCoreReferenceFinder msCoreReferenceFinder)
        {
            this.weavingTask = weavingTask;
            this.interceptorFinder = interceptorFinder;
            this.delegateHolderInjector = delegateHolderInjector;
            this.msCoreReferenceFinder = msCoreReferenceFinder;
        }

        public EventInvokerMethod AddOnPropertyChangedMethod(TypeDefinition targetType)
        {
            var propertyChangedField = FindPropertyChangedField(targetType);
            if (propertyChangedField == null)
            {
                return null;
            }
            if (interceptorFinder.Found)
            {
                var methodDefinition = GetMethodDefinition(targetType, propertyChangedField);

                return new EventInvokerMethod
                           {
                               MethodReference = InjectIntercepetedMethod(targetType, methodDefinition).GetGeneric(),
                               IsBeforeAfter = interceptorFinder.IsBeforeAfter,
                               IsVirtual = true,
                           };
            }
            return new EventInvokerMethod
                       {
                           MethodReference = InjectMethod(targetType, weavingTask.EventInvokerName, propertyChangedField).GetGeneric(),
                           IsBeforeAfter = false,
                           IsVirtual = true,
                       };
        }

        MethodDefinition GetMethodDefinition(TypeDefinition targetType, FieldReference propertyChangedField)
        {
            var eventInvokerName = "Inner" + weavingTask.EventInvokerName;
            var methodDefinition = targetType.Methods.FirstOrDefault(x => x.Name == eventInvokerName);
            if (methodDefinition != null)
            {
                if (methodDefinition.Parameters.Count == 1 && methodDefinition.Parameters[0].ParameterType.FullName == "System.String")
                {
                    return methodDefinition;
                }
            }
            return InjectMethod(targetType, eventInvokerName, propertyChangedField);
        }

        MethodDefinition InjectMethod(TypeDefinition targetType, string eventInvokerName, FieldReference propertyChangedField)
        {
            var method = new MethodDefinition(eventInvokerName, GetMethodAttributes(targetType), msCoreReferenceFinder.VoidTypeReference);
            method.Parameters.Add(new ParameterDefinition("propertyName", ParameterAttributes.None, msCoreReferenceFinder.StringTypeReference));

            var handlerVariable = new VariableDefinition(propertyChangedField.FieldType);
            method.Body.Variables.Add(handlerVariable);
            var boolVariable = new VariableDefinition(msCoreReferenceFinder.BoolTypeReference);
            method.Body.Variables.Add(boolVariable);

            var instructions = method.Body.Instructions;

            var last = Instruction.Create(OpCodes.Ret);
            instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
            instructions.Add(Instruction.Create(OpCodes.Ldfld, propertyChangedField));
            instructions.Add(Instruction.Create(OpCodes.Stloc_0));
            instructions.Add(Instruction.Create(OpCodes.Ldloc_0));
            instructions.Add(Instruction.Create(OpCodes.Ldnull));
            instructions.Add(Instruction.Create(OpCodes.Ceq));
            instructions.Add(Instruction.Create(OpCodes.Stloc_1));
            instructions.Add(Instruction.Create(OpCodes.Ldloc_1));
            instructions.Add(Instruction.Create(OpCodes.Brtrue_S, last));
            instructions.Add(Instruction.Create(OpCodes.Ldloc_0));
            instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
            instructions.Add(Instruction.Create(OpCodes.Ldarg_1));
            if (propertyChangedField.FieldType.Namespace.StartsWith("System.C"))
            {
                instructions.Add(Instruction.Create(OpCodes.Newobj, msCoreReferenceFinder.ComponentModelPropertyChangedEventConstructorReference));
                instructions.Add(Instruction.Create(OpCodes.Callvirt, msCoreReferenceFinder.ComponentModelPropertyChangedEventHandlerInvokeReference));
            }
            else
            {
                instructions.Add(Instruction.Create(OpCodes.Newobj, msCoreReferenceFinder.XamlPropertyChangedEventConstructorReference));
                instructions.Add(Instruction.Create(OpCodes.Callvirt, msCoreReferenceFinder.XamlPropertyChangedEventHandlerInvokeReference));
            }

            instructions.Add(last);
            method.Body.InitLocals = true;
            targetType.Methods.Add(method);
            return method;
        }

        static MethodAttributes GetMethodAttributes(TypeDefinition targetType)
        {
            if (targetType.IsSealed)
            {
                return MethodAttributes.Public | MethodAttributes.HideBySig;
            }
            return MethodAttributes.Virtual | MethodAttributes.Public | MethodAttributes.NewSlot;
        }

        public static FieldReference FindPropertyChangedField(TypeDefinition targetType)
        {
            var findPropertyChangedField = targetType.Fields.FirstOrDefault(x=>NotifyInterfaceFinder.IsPropertyChangedEventHandler(x.FieldType));
            if (findPropertyChangedField == null)
            {
                return null;
            }
            return findPropertyChangedField.GetGeneric();
        }

        MethodDefinition InjectIntercepetedMethod(TypeDefinition targetType, MethodDefinition innerOnPropertyChanged)
        {
            delegateHolderInjector.Execute(targetType, innerOnPropertyChanged);
            var method = new MethodDefinition(weavingTask.EventInvokerName, GetMethodAttributes(targetType), msCoreReferenceFinder.VoidTypeReference);

            var propertyName = new ParameterDefinition("propertyName", ParameterAttributes.None, msCoreReferenceFinder.StringTypeReference);
            method.Parameters.Add(propertyName);
            if (interceptorFinder.IsBeforeAfter)
            {
                var before = new ParameterDefinition("before", ParameterAttributes.None, msCoreReferenceFinder.ObjectTypeReference);
                method.Parameters.Add(before);
                var after = new ParameterDefinition("after", ParameterAttributes.None, msCoreReferenceFinder.ObjectTypeReference);
                method.Parameters.Add(after);
            }

            var action = new VariableDefinition("firePropertyChanged", msCoreReferenceFinder.ActionTypeReference);
            method.Body.Variables.Add(action);

            var variableDefinition = new VariableDefinition("delegateHolder", delegateHolderInjector.TypeDefinition);
            method.Body.Variables.Add(variableDefinition);


            var instructions = method.Body.Instructions;

            var last = Instruction.Create(OpCodes.Ret);
            instructions.Add(Instruction.Create(OpCodes.Newobj, delegateHolderInjector.ConstructorDefinition));
            instructions.Add(Instruction.Create(OpCodes.Stloc_1));
            instructions.Add(Instruction.Create(OpCodes.Ldloc_1));
            instructions.Add(Instruction.Create(OpCodes.Ldarg_1));
            instructions.Add(Instruction.Create(OpCodes.Stfld, delegateHolderInjector.PropertyName));
            instructions.Add(Instruction.Create(OpCodes.Ldloc_1));
            instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
            instructions.Add(Instruction.Create(OpCodes.Stfld, delegateHolderInjector.Target));
            instructions.Add(Instruction.Create(OpCodes.Ldloc_1));
            instructions.Add(Instruction.Create(OpCodes.Ldftn, delegateHolderInjector.MethodDefinition));
            instructions.Add(Instruction.Create(OpCodes.Newobj, msCoreReferenceFinder.ActionConstructorReference));
            instructions.Add(Instruction.Create(OpCodes.Stloc_0));
            instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
            instructions.Add(Instruction.Create(OpCodes.Ldloc_0));
            instructions.Add(Instruction.Create(OpCodes.Ldloc_1));
            instructions.Add(Instruction.Create(OpCodes.Ldfld, delegateHolderInjector.PropertyName));
            if (interceptorFinder.IsBeforeAfter)
            {
                instructions.Add(Instruction.Create(OpCodes.Ldarg_2));
                instructions.Add(Instruction.Create(OpCodes.Ldarg_3));
                instructions.Add(Instruction.Create(OpCodes.Call, interceptorFinder.InterceptMethod));
            }
            else
            {
                instructions.Add(Instruction.Create(OpCodes.Call, interceptorFinder.InterceptMethod));
            }

            instructions.Add(last);
            method.Body.InitLocals = true;

            targetType.Methods.Add(method);
            return method;
        }
    }
}